/*
 * $Id: muscl.y 352 2010-05-07 17:02:43Z cmtonkinson@gmail.com $
 *
 * This file is part of the Symphony project <http://code.google.com/p/symphonymud/>
 * Copyright 2005-2010 Chris Tonkinson <cmtonkinson@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

%{

#include <stdio.h>
#include <string>
#include "bones-vm.h"
#include "muscl-ast.h"
#include "muscl-ast-assignment.h"
#include "muscl-ast-binary.h"
#include "muscl-ast-conditional.h"
#include "muscl-ast-declaration.h"
#include "muscl-ast-expr-list.h"
#include "muscl-ast-func-call.h"
#include "muscl-ast-func-decl.h"
#include "muscl-ast-jump.h"
#include "muscl-ast-list.h"
#include "muscl-ast-loop.h"
#include "muscl-ast-param-list.h"
#include "muscl-ast-reference.h"
#include "muscl-ast-send.h"
#include "muscl-ast-stmt-list.h"
#include "muscl-ast-unary.h"
#include "muscl-ast-value.h"
#include "muscl-datum.h"
#include "muscl-scope.h"
#include "muscl-symbol.h"

#define YYSTYPE Ast*

int yylex (void);
extern char* yytext;
extern int yylineno;

void yyerror( char const* s ) {
  printf( "muscl: error on line %d: %s\n", yylineno, s );
  exit( 0 );
}

Scope scope;
Ast* root;
VirtualMachine bones;

std::string decl_info( Symbol* sym ) {
  char buf[256];
  sprintf( buf, "it was declared on line %d as a %s", sym->line(), sym->stringType() );
  return std::string( buf );
}

AstReference* assertCreate( char* token, const unsigned& type, const char* strtype ) {
  char buf[256];
  Symbol* sym = scope.lookup( token );
  if ( sym ) {
    sprintf( buf, "Cannot redeclare '%s' as a %s (%s)", token, strtype, decl_info( sym ).c_str() );
    yyerror( buf );
  }
  sym = new Symbol( token, type, yylineno );
  scope.insert( sym );
  return new AstReference( sym );
}

AstReference* assertLookup( char* token ) {
  char buf[256];
  Symbol* sym = scope.lookup( token );
  if ( !sym ) {
    sprintf( buf, "Attempting to use undefined symbol '%s' on line %d", token, yylineno );
    yyerror( buf );
  }
  return new AstReference( sym );
}

void assertVariable( AstReference* ref ) {
  if ( !ref->symbol()->isVariable() ) {
    std::string err( "Cannot mutate '" );
    err.append( ref->symbol()->name() );
    err.append( "' (" ).append( decl_info( ref->symbol() ) ).append( ")." );
    yyerror( err.c_str() );
  }
  return;
}

void assertFunction( AstReference* ref ) {
  if ( !ref->symbol()->isFunction() ) {
    std::string err( "Cannot call '" );
    err.append( ref->symbol()->name() );
    err.append( "' as as function (" ).append( decl_info( ref->symbol() ) ).append( ")." );
    yyerror( err.c_str() );
  }
  return;
}

%}

%token IDENTIFIER
%token FUNCTION
%token INT REAL STRING CONST VAR
%token OP_LE OP_GE OP_EQ OP_NE
%token AND OR NOT
%token MUL_ASSIGN DIV_ASSIGN MOD_ASSIGN ADD_ASSIGN SUB_ASSIGN POW_ASSIGN
%token IF UNLESS ELSE WHILE UNTIL DO FOR IN FROM TO
%token CONTINUE BREAK RETURN
%token SEND

%%

script
  : statement_list                                        { root = $1; }
  ;

statement_list
  : statement                                             { $$ = new AstStatementList( $1, NULL ); }
  | statement statement_list                              { $$ = new AstStatementList( $1, (AstStatementList*)$2 ); }
  ;

statement
  : declaration                                           { $$ = $1; }
  | assignment ';'                                        { $$ = $1; }
  | conditional_statement                                 { $$ = $1; }
  | loop_statement                                        { $$ = $1; }
  | jump_statement ';'                                    { $$ = $1; }
  | function_call ';'                                     { $$ = $1; }
  | send_statement ';'                                    { $$ = $1; }
  ;

conditional_statement
  : IF '(' expression ')' block                           { $$ = new AstConditional( AST_CONT_IF, $3, $5, NULL ); }
  | IF '(' expression ')' block ELSE block                { $$ = new AstConditional( AST_CONT_IF_ELSE, $3, $5, $7 ); }
  | UNLESS '(' expression ')' block                       { $$ = new AstConditional( AST_CONT_UNLESS, $3, $5, NULL ); }
  ;

block
  : '{' statement_list '}'                                { $$ = $2; }
  ;

loop_statement
  : WHILE '(' expression ')' block                            { $$ = new AstLoop( AST_CONT_WHILE, NULL, $3, NULL, $5 ); }
  | DO block WHILE '(' expression ')'                         { $$ = new AstLoop( AST_CONT_DO_WHILE, NULL, $5, NULL, $2 ); }
  | UNTIL '(' expression ')' block                            { $$ = new AstLoop( AST_CONT_UNTIL, NULL, $3, NULL, $5 ); }
  | DO block UNTIL '(' expression ')'                         { $$ = new AstLoop( AST_CONT_DO_UNTIL, NULL, $5, NULL, $2 ); }
  | FOR     id_lookup     FROM expression TO expression block { $$ = new AstLoop( AST_CONT_FOR_TO, (AstReference*)$2, $4, $6, $7 ); }
  | FOR VAR id_create_var FROM expression TO expression block { $$ = new AstLoop( AST_CONT_FOR_TO, (AstReference*)$3, $5, $7, $8 ); }
  | FOR     id_lookup     IN expression block                 { $$ = new AstLoop( AST_CONT_FOR_IN, (AstReference*)$2, $4, NULL, $5 ); }
  | FOR VAR id_create_var IN expression block                 { $$ = new AstLoop( AST_CONT_FOR_IN, (AstReference*)$3, $5, NULL, $6 ); }
  ;

jump_statement
  : CONTINUE                                              { $$ = new AstJump( AST_JUMP_CONT ); }
  | BREAK                                                 { $$ = new AstJump( AST_JUMP_BREAK ); }
  | RETURN                                                { $$ = new AstJump( AST_FUNC_RETURN ); }
  | RETURN expression                                     { $$ = new AstJump( AST_FUNC_RETURN, $2 ); }
  ;

function_call
  : id_lookup '(' expression_list ')' {
      assertFunction( (AstReference*)$1 );
      $$ = new AstFunctionCall( (AstReference*)$1, (AstExpressionList*)$3 );
    }
  ;

parameter_list
  :                                                       { $$ = new AstParameterList( NULL, NULL ); }
  | id_create_var                                         { $$ = new AstParameterList( (AstReference*)$1, NULL ); }
  | id_create_var ',' parameter_list                      { $$ = new AstParameterList( (AstReference*)$1, (AstParameterList*)$3 ); }
  ;

id_create_var
  : IDENTIFIER                                            { $$ = assertCreate( yytext, SYMBOL_VAR, "variable" ); }
  ;

id_create_const
  : IDENTIFIER                                            { $$ = assertCreate( yytext, SYMBOL_CONST, "constant" ); }
  ;

id_create_func
  : IDENTIFIER                                            { $$ = assertCreate( yytext, SYMBOL_FUNC, "function" ); }
  ;

id_lookup
  : IDENTIFIER                                            { $$ = assertLookup( yytext ); }
  ;

send_statement
  : SEND expression                                       { $$ = new AstSend( $2 ); }
  ;

declaration
  : CONST id_create_const '=' expression ';'              { $$ = new AstDeclaration( AST_CONST, (AstReference*)$2, $4 ); }
  | VAR id_create_var '=' expression ';'                  { $$ = new AstDeclaration( AST_VAR, (AstReference*)$2, $4 ); }
  | FUNCTION id_create_func {
      char buf[256];
      sprintf( buf, "user-func-%s", ((AstReference*)$2)->symbol()->name().c_str() );
      scope.pushFrame( buf );
    }
    '(' parameter_list ')' block {
      $$ = new AstFunctionDeclaration( (AstReference*)$2, (AstParameterList*)$5, $7, scope.popFrame() );
    }
  ;

assignment
  : id_lookup '=' expression                              { assertVariable( (AstReference*)$1 ); $$ = new AstAssignment( AST_ASS,     (AstReference*)$1, $3 ); }
  | id_lookup ADD_ASSIGN expression                       { assertVariable( (AstReference*)$1 ); $$ = new AstAssignment( AST_ASS_ADD, (AstReference*)$1, $3 ); }
  | id_lookup SUB_ASSIGN expression                       { assertVariable( (AstReference*)$1 ); $$ = new AstAssignment( AST_ASS_SUB, (AstReference*)$1, $3 ); }
  | id_lookup MUL_ASSIGN expression                       { assertVariable( (AstReference*)$1 ); $$ = new AstAssignment( AST_ASS_MUL, (AstReference*)$1, $3 ); }
  | id_lookup DIV_ASSIGN expression                       { assertVariable( (AstReference*)$1 ); $$ = new AstAssignment( AST_ASS_DIV, (AstReference*)$1, $3 ); }
  | id_lookup MOD_ASSIGN expression                       { assertVariable( (AstReference*)$1 ); $$ = new AstAssignment( AST_ASS_MOD, (AstReference*)$1, $3 ); }
  | id_lookup POW_ASSIGN expression                       { assertVariable( (AstReference*)$1 ); $$ = new AstAssignment( AST_ASS_POW, (AstReference*)$1, $3 ); }
  ;

primary_expression
  : id_lookup                                             { $$ = $1; }
  | constant_value                                        { $$ = $1; }
  | '(' expression ')'                                    { $$ = $2; }
  | list_initialization                                   { $$ = $1; }
  | function_call                                         { $$ = $1; }
  ;

constant_value
  : INT {
      long l = 0;
      sscanf( yytext, "%ld", &l );
      $$ = new AstValue( new Datum( l ) );
    }
  | REAL {
      double d = 0;
      sscanf( yytext, "%lf", &d );
      $$ = new AstValue( new Datum( d ) );
    }
  | STRING {
      /* the witch-trickery here is to efficiently trim the double quotes off of yytext */
      yytext[strlen(yytext)-1] = 0;
      $$ = new AstValue( new Datum( yytext+1 ) );
    }
  ;

list_initialization
  : '[' expression_list ']'                               { $$ = new AstList( (AstExpressionList*)$2 ); }
  ;

expression_list
  :                                                       { $$ = new AstExpressionList( NULL, NULL ); }
  | expression                                            { $$ = new AstExpressionList( $1, NULL ); }
  | expression ',' expression_list                        { $$ = new AstExpressionList( $1, (AstExpressionList*)$3 ); }
  ;

unary_expression
  : primary_expression                                    { $$ = $1; }
  | '-' unary_expression                                  { $$ = new AstUnaryOperation( AST_UN_MIN, $2 ); }
  | NOT unary_expression                                  { $$ = new AstUnaryOperation( AST_UN_NOT, $2 ); }
  | '|' unary_expression '|'                              { $$ = new AstUnaryOperation( AST_UN_CARD, $2 ); }
  ;

exponential_expression
  : unary_expression                                      { $$ = $1; }
  | exponential_expression '^' unary_expression           { $$ = new AstBinaryOperation( AST_BIN_POW, $1, $3 ); }
  ;

multiplicative_expression
  : exponential_expression                                { $$ = $1; }
  | multiplicative_expression '*' exponential_expression  { $$ = new AstBinaryOperation( AST_BIN_MUL, $1, $3 ); }
  | multiplicative_expression '/' exponential_expression  { $$ = new AstBinaryOperation( AST_BIN_DIV, $1, $3 ); }
  | multiplicative_expression '%' exponential_expression  { $$ = new AstBinaryOperation( AST_BIN_MOD, $1, $3 ); }
  ;

additive_expression
  : multiplicative_expression                             { $$ = $1; }
  | additive_expression '+' multiplicative_expression     { $$ = new AstBinaryOperation( AST_BIN_ADD, $1, $3 ); }
  | additive_expression '-' multiplicative_expression     { $$ = new AstBinaryOperation( AST_BIN_SUB, $1, $3 ); }
  ;

relational_expression
  : additive_expression                                   { $$ = $1; }
  | relational_expression '<' additive_expression         { $$ = new AstBinaryOperation( AST_BIN_LT, $1, $3 ); }
  | relational_expression '>' additive_expression         { $$ = new AstBinaryOperation( AST_BIN_GT, $1, $3 ); }
  | relational_expression OP_LE additive_expression       { $$ = new AstBinaryOperation( AST_BIN_LE, $1, $3 ); }
  | relational_expression OP_GE additive_expression       { $$ = new AstBinaryOperation( AST_BIN_GE, $1, $3 ); }
  ;

equality_expression
  : relational_expression                                 { $$ = $1; }
  | equality_expression OP_EQ relational_expression       { $$ = new AstBinaryOperation( AST_BIN_EQ, $1, $3 ); }
  | equality_expression OP_NE relational_expression       { $$ = new AstBinaryOperation( AST_BIN_NE, $1, $3 ); }
  ;

logical_and_expression
  : equality_expression                                   { $$ = $1; }
  | logical_and_expression AND equality_expression        { $$ = new AstBinaryOperation( AST_BIN_AND, $1, $3 ); }
  ;

logical_or_expression
  : logical_and_expression                                { $$ = $1; }
  | logical_or_expression OR logical_and_expression       { $$ = new AstBinaryOperation( AST_BIN_OR, $1, $3 ); }
  ;

expression
  : logical_or_expression                                 { $$ = $1; }
  ;

%%

int main( void ) {
  root = NULL;
  scope.pushFrame( "built-in" );  // namespace for system functions and constants
  scope.pushFrame( "global" );    // default namespace for programs
  yyparse();
  root->print( "", true );
  printf( "\n" );
  root->codegen();
  root->dumpcode();
  bones.run( &root->code() );
  delete root;
}
